﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace EditoneLib.Streams
{
    public class ProjectStream : ReadonlyStream
    {
        private readonly Project _project;

        private long _position;

        private TimeSpan _totalTime;

        internal ProjectStream(Project project, ChannelMapping mapping)
        {
            _project = project;
            Mapping = mapping;
            _totalTime = ComputeTime();
        }

        public event EventHandler<long> PositionChanged;

        public override long Length
        {
            get
            {
                var second = Settings.SampleRate*Mapping.NumChannels*Settings.SampleSize;
                return _totalTime.Ticks*second/10000000;
            }
        }

        public ChannelMapping Mapping { get; set; }

        public override long Position
        {
            get { return _position; }
            set
            {
                bool changed = _position != value;
                _position = value;
                if (changed && PositionChanged != null)
                {
                    PositionChanged(this, value);
                }
            }
        }

        public Project Project
        {
            get { return _project; }
        }

        public override int Read(byte[] byteBuffer, int offset, int count)
        {
            /**
             * PRELUDE: declare
             * Declare anything ahead of time
             */
            var blockAlign = Mapping.NumChannels*Settings.SampleSize;
            var blockCount = count/blockAlign;
            var second = Settings.SampleRate*blockAlign;
            var startTime = TimeSpan.FromTicks(Position*10000000/second);
            var length = TimeSpan.FromTicks((long) count*10000000/second);
            var endTime = startTime.Add(length);

            /**
             * PHASE 1: Target
             * Gather clips, their offsets in this buffer, the offsets from the start of their streams and their channel mappings
             */
            var clipInfo = GatherClipInfo(startTime, ref endTime);

            /**
             * PHASE 2: Act
             * Get the clips float values for each sample. Sum it all up for each channel [1 - Mapping.NumChannels] (inclusive).
             */
            var floatBuffer = GetFloats(blockCount, clipInfo);

            /**
             * PHASE 3: Return
             * Copy into given byte buffer and return number of read bytes.
             */
            Buffer.BlockCopy(floatBuffer, 0, byteBuffer, 0, count);

            var bytesRead = endTime.Subtract(startTime).Ticks*
                            (Mapping.NumChannels*Settings.SampleRate*Settings.SampleSize)/10000000;
            Position += bytesRead;
            var intBytesRead = (int) bytesRead;
            return intBytesRead;
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            switch (origin)
            {
                case SeekOrigin.Begin:
                    Position = offset;
                    break;

                case SeekOrigin.Current:
                    Position += offset;
                    break;

                case SeekOrigin.End:
                    Position = Length - offset;
                    break;

                default:
                    throw new ArgumentOutOfRangeException("origin", origin, null);
            }
            return Position;
        }

        private TimeSpan ComputeTime()
        {
            TimeSpan maxOffset = TimeSpan.Zero;
            foreach (var anyTrack in Project)
            {
                var track = anyTrack as IAudioTrack;
                if (track == null) continue;

                foreach (var kvp in track)
                {
                    var clipEnd = kvp.Key + kvp.Value.TotalLength;

                    if (clipEnd > maxOffset)
                    {
                        maxOffset = clipEnd;
                    }
                }
            }
            return maxOffset;
        }

        private IEnumerable<ClipInfo> GatherClipInfo(TimeSpan startTime, ref TimeSpan endTime)
        {
            var list = new LinkedList<ClipInfo>();

            TimeSpan maxOffset = TimeSpan.Zero;
            foreach (var anyTrack in Project)
            {
                var track = anyTrack as IAudioTrack;
                if (track == null) continue;

                foreach (var kvp in track)
                {
                    var clipEnd = kvp.Key + kvp.Value.TotalLength;

                    if (clipEnd > maxOffset)
                    {
                        maxOffset = clipEnd;
                    }

                    if ((kvp.Key > endTime || clipEnd < startTime))
                    {
                        continue;
                    }

                    GetInfoFor(kvp, list, startTime, track);
                }
            }
            if (endTime > maxOffset)
            {
                endTime = maxOffset;
            }
            return list;
        }

        private float[] GetFloats(int blockCount, IEnumerable<ClipInfo> clipInfo)
        {
            var numChannels = Mapping.NumChannels;
            float[] buffer = new float[blockCount*numChannels];

            var infoArray = clipInfo as ClipInfo[] ?? clipInfo.ToArray();
            var clipBuffers = new float[infoArray.Length][];

            var projectVolume = (float) Project.Settings["Volume"];
            var channelVolumes = new float[numChannels];

            for (int ch = 0; ch < numChannels; ch++)
            {
                var factor = (float)Project.Settings["Volume"];
                var key = "Channel." + (ch + 1) + ".Volume";
                if (Project.Settings.ContainsKey(key))
                {
                    factor *= (float)Project.Settings[key];
                }
            }

            for (int i=0; i<infoArray.Length; i++)
            {
                var info = infoArray[i];
                var clipBuffer = new byte[blockCount*info.Clip.BlockAlign];
                int count;

                lock (info.Clip.Stream)
                {
                    info.Clip.Stream.Position = info.ClipBlockOffset*info.Clip.BlockAlign;
                    count = info.Clip.Stream.Read(clipBuffer, 0,
                        (blockCount - info.BufferBlockOffset)*info.Clip.BlockAlign);
                }

                var floatBuffer = new float[count/Settings.SampleSize];
                Buffer.BlockCopy(clipBuffer, 0, floatBuffer, 0, count);
                clipBuffers[i] = floatBuffer;
            }

            for (int i = 0; i < blockCount; i++)
            {
                int startOffset = i * numChannels; 
                for (int k = 0; k < infoArray.Length; k++)
                {
                    var info = infoArray[k];
                    var clipBuffer = clipBuffers[k];
                    var clipChannels = info.Clip.NumChannels;
                    if (info.BufferBlockOffset > i) continue;
                    if (info.BufferBlockOffset + clipBuffer.Length/clipChannels <= i) continue;

                    for (int ch = 0; ch < clipChannels; ch++)
                    {
                        foreach (var mapping in info.MappingInfo)
                        {
                            if (mapping.Offset != ch)
                            {
                                continue;
                            }

                            if (mapping.Offset == ChannelMapping.All)
                            {
                                for (int j = 0; j < numChannels; j++)
                                {
                                    buffer[startOffset + j] +=
                                        clipBuffer[(i - info.BufferBlockOffset) * clipChannels + ch] 
                                        * mapping.Amount;
                                }
                            }
                            else
                            {
                                buffer[startOffset + mapping.Offset - 1] +=
                                    clipBuffer[(i - info.BufferBlockOffset) * clipChannels + ch]
                                    * mapping.Amount;
                            }
                        }
                    }
                }
                // Check for Project.Settings["Channel.X.Volume"] and multiply by that.
                // Multiply each sum by Project.Settings["Volume"].
                for (int ch = 0; ch < numChannels; ch++)
                {
                    var factor = (float)Project.Settings["Volume"];
                    var key = "Channel." + (ch + 1) + ".Volume";
                    if (Project.Settings.ContainsKey(key))
                    {
                        factor *= (float)Project.Settings[key];
                    }

                    buffer[startOffset + ch] *= factor;
                }
            }
            return buffer;
        }

        private void GetInfoFor(KeyValuePair<TimeSpan, IClip> kvp, LinkedList<ClipInfo> list, TimeSpan startTime,
            IAudioTrack track)
        {
            var bufferBlockOffset = 0;
            var clipBlockOffset = 0;

            if (kvp.Key > startTime)
            {
                bufferBlockOffset = (int) (kvp.Key.Subtract(startTime).Ticks*Settings.SampleRate/10000000);
            }

            if (kvp.Key < startTime)
            {
                clipBlockOffset = (int) (startTime.Subtract(kvp.Key).Ticks*Settings.SampleRate/10000000);
            }

            if (!(kvp.Value is BaseAudioClip))
            {
                throw new InvalidOperationException("Every IClip in IAudioTrack must inherit from BaseAudioClip");
            }

            list.AddLast(new ClipInfo()
            {
                BufferBlockOffset = bufferBlockOffset,
                Clip = (BaseAudioClip) kvp.Value,
                ClipBlockOffset = clipBlockOffset,
                MappingInfo = Mapping[track]
            });
        }

        private struct ClipInfo
        {
            public int BufferBlockOffset;
            public BaseAudioClip Clip;
            public int ClipBlockOffset;
            public IEnumerable<MappingInfo> MappingInfo;
        }
    }
}